/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.primitives.stdlib;

import fr.x9c.cadmium.kernel.Block;
import fr.x9c.cadmium.kernel.CodeRunner;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.Primitive;
import fr.x9c.cadmium.kernel.PrimitiveProvider;
import fr.x9c.cadmium.kernel.Value;

/**
 * Implements all primitives from 'array.c'.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.0
 * @since 1.0
 */
@PrimitiveProvider
public final class Array {

    /**
     * No instance of this class.
     */
    private Array() {
    } // end empty constructor

    /**
     * Returns a value from a block viewed as an array.
     * @param ctxt context
     * @param array block/array
     * @param index index of value to return
     * @return the value of the block/array at the given index
     * @throws Fail.Exception <i>Invalid_argument("index out of bounds")</i> if index is out of bounds
     */
    @Primitive
    public static Value caml_array_get_addr(final CodeRunner ctxt,
                                            final Value array,
                                            final Value index)
        throws Fail.Exception {
        final Block block = array.asBlock();
        final int idx = index.asLong();
        if ((idx < 0) || (idx >= block.sizeValues())) {
            Fail.arrayBoundError();
        } // end if
        return block.get(idx);
    } // end method 'caml_array_get_addr(CodeRunner, Value, Value)'

    /**
     * Returns a float from a block viewed as an array.
     * @param ctxt context
     * @param array float block/array
     * @param index index of value to return
     * @return a copy of the float of the block/array at the given index
     * @throws Fail.Exception <i>Invalid_argument("index out of bounds")</i> if index is out of bounds
     */
    @Primitive
    public static Value caml_array_get_float(final CodeRunner ctxt,
                                             final Value array,
                                             final Value index)
        throws Fail.Exception {
        final Block block = array.asBlock();
        final int idx = index.asLong();
        if ((idx < 0) || (idx >= block.sizeDoubles())) {
            Fail.arrayBoundError();
        } // end if
        return Value.createFromBlock(Block.createDouble(block.getDouble(idx)));
    } // end method 'caml_array_get_float(CodeRunner, Value, Value)'

    /**
     * Returns a float or value from a block viewed as an array. <br/>
     * Calls {@link #caml_array_get_float(fr.x9c.cadmium.kernel.CodeRunner, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value)} if the array
     * is a float array, {@link #caml_array_get_addr(fr.x9c.cadmium.kernel.CodeRunner, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value)}.
     * otherwise.
     * @param ctxt context
     * @param array block/array
     * @param index index of float or value to return
     * @return the float or value of the block/array at the given index
     * @throws Fail.Exception <i>Invalid_argument("index out of bounds")</i> if index is out of bounds
     */
    @Primitive
    public static Value caml_array_get(final CodeRunner ctxt,
                                       final Value array,
                                       final Value index)
        throws Fail.Exception {
        if (array.asBlock().getTag() == Block.DOUBLE_ARRAY_TAG) {
            return caml_array_get_float(ctxt, array, index);
        } else {
            return caml_array_get_addr(ctxt, array, index);
        } // end if/else
    } // end method 'caml_array_get(CodeRunner, Value, Value)'

    /**
     * Changes a value in a block viewed as an array.
     * @param ctxt context
     * @param array block/array
     * @param index index of value to modify
     * @param val new value
     * @return <i>unit</i>
     * @throws Fail.Exception <i>Invalid_argument("index out of bounds")</i> if index is out of bounds
     */
    @Primitive
    public static Value caml_array_set_addr(final CodeRunner ctxt,
                                            final Value array,
                                            final Value index,
                                            final Value val)
        throws Fail.Exception {
        final Block block = array.asBlock();
        final int idx = index.asLong();
        if ((idx < 0) || (idx >= block.sizeValues())) {
            Fail.arrayBoundError();
        } // end if
        block.set(idx, val);
        return Value.UNIT;
    } // end method 'caml_array_set_addr(CodeRunner, Value, Value, Value)'

    /**
     * Changes a float in a block viewed as an array.
     * @param ctxt context
     * @param array block/array
     * @param index index of value to modify
     * @param val new float value (a copy is stored)
     * @return <i>unit</i>
     * @throws Fail.Exception <i>Invalid_argument("index out of bounds")</i> if index is out of bounds
     */
    @Primitive
    public static Value caml_array_set_float(final CodeRunner ctxt,
                                             final Value array,
                                             final Value index,
                                             final Value val)
        throws Fail.Exception {
        final Block block = array.asBlock();
        final int idx = index.asLong();
        if ((idx < 0) || (idx >= block.sizeDoubles())) {
            Fail.arrayBoundError();
        } // end if
        block.setDouble(idx, val.asBlock().asDouble());
        return Value.UNIT;
    } // end method 'caml_array_set_float(CodeRunner, Value, Value, Value)'

    /**
     * Changes a float or value in a block viewed as an array. <br/>
     * Calls {@link #caml_array_set_float(fr.x9c.cadmium.kernel.CodeRunner, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value)} if the array is a float array, {@link #caml_array_set_addr(fr.x9c.cadmium.kernel.CodeRunner, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value)} otherwise.
     * @param ctxt context
     * @param array block/array
     * @param index index of value to modify
     * @param val new float or value
     * @return <i>unit</i>
     * @throws Fail.Exception <i>Invalid_argument("index out of bounds")</i> if index is out of bounds
     */
    @Primitive
    public static Value caml_array_set(final CodeRunner ctxt,
                                       final Value array,
                                       final Value index,
                                       final Value val)
        throws Fail.Exception {
        if (array.asBlock().getTag() == Block.DOUBLE_ARRAY_TAG) {
            return caml_array_set_float(ctxt, array, index, val);
        } else {
            return caml_array_set_addr(ctxt, array, index, val);
        } // end if/else
    } // end method 'caml_array_set(CodeRunner, Value, Value, Value)'

    /**
     * Returns a float from a block viewed as an array. <br/>
     * <b>Index is not checked for bounds.</b>
     * @param ctxt context
     * @param array block/array
     * @param index index of value to return
     * @return a copy of the float of the block/array at the given index
     */
    @Primitive
    public static Value caml_array_unsafe_get_float(final CodeRunner ctxt,
                                                    final Value array,
                                                    final Value index) {
        return Value.createFromBlock(Block.createDouble(array.asBlock().getDouble(index.asLong())));
    } // end method 'caml_array_unsafe_get_float(CodeRunner, Value, Value)'

    /**
     * Returns a float or value from a block viewed as an array. <br/>
     * <b>Index is not checked for bounds.</b> <br/>
     * Calls {@link #caml_array_unsafe_get_float(fr.x9c.cadmium.kernel.CodeRunner, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value)} if the array
     * is a float array.
     * @param ctxt context
     * @param array block/array
     * @param index index of float or value to return
     * @return the float or value of the block/array at the given index
     */
    @Primitive
    public static Value caml_array_unsafe_get(final CodeRunner ctxt,
                                              final Value array,
                                              final Value index) {
        if (array.asBlock().getTag() == Block.DOUBLE_ARRAY_TAG) {
            return caml_array_unsafe_get_float(ctxt, array, index);
        } else {
            return array.asBlock().get(index.asLong());
        } // end if/else
    } // end method 'caml_array_unsafe_get(CodeRunner, Value, Value)'

    /**
     * Changes a value in a block viewed as an array. <br/>
     * <b>Index is not checked for bounds.</b>
     * @param ctxt context
     * @param array block/array
     * @param index index of value to modify
     * @param val new value
     * @return <i>unit</i>
     */
    @Primitive
    public static Value caml_array_unsafe_set_addr(final CodeRunner ctxt,
                                                   final Value array,
                                                   final Value index,
                                                   final Value val) {
        array.asBlock().set(index.asLong(), val);
        return Value.UNIT;
    } // end method 'caml_array_unsafe_set_addr(CodeRunner, Value, Value, Value)'

    /**
     * Changes a float in a block viewed as an array. <br/>
     * <b>Index is not checked for bounds.</b>
     * @param ctxt context
     * @param array block/array
     * @param index index of value to modify
     * @param val new float value (a copy is stored)
     * @return <i>unit</i>
     */
    @Primitive
    public static Value caml_array_unsafe_set_float(final CodeRunner ctxt,
                                                    final Value array,
                                                    final Value index,
                                                    final Value val) {
        array.asBlock().setDouble(index.asLong(), val.asBlock().asDouble());
        return Value.UNIT;
    } // end method 'caml_array_unsafe_set_float(CodeRunner, Value, Value, Value)'

    /**
     * Changes a float or value in a block viewed as an array. <br/>
     * <b>Index is not checked for bounds.</b> <br/>
     * Calls {@link #caml_array_unsafe_set_float(fr.x9c.cadmium.kernel.CodeRunner, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value)} if the array is a float array, {@link #caml_array_unsafe_set_addr(fr.x9c.cadmium.kernel.CodeRunner, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value, fr.x9c.cadmium.kernel.Value)} otherwise.
     * @param ctxt context
     * @param array block/array
     * @param index index of value to modify
     * @param val new float or value
     * @return <i>unit</i>
     */
    @Primitive
    public static Value caml_array_unsafe_set(final CodeRunner ctxt,
                                              final Value array,
                                              final Value index,
                                              final Value val) {
        if (array.asBlock().getTag() == Block.DOUBLE_ARRAY_TAG) {
            return caml_array_unsafe_set_float(ctxt, array, index, val);
        } else {
            return caml_array_unsafe_set_addr(ctxt, array, index, val);
        } // end if/else
    } // end method 'caml_array_unsafe_set(CodeRunner, Value, Value, Value)'

    /**
     * Creates a block/array.
     * @param ctxt context
     * @param len size of block/array
     * @param init initial value of each block/array element
     * @return array built
     * @throws Fail.Exception if length is too big
     */
    @Primitive
    public static Value caml_make_vect(final CodeRunner ctxt,
                                       final Value len,
                                       final Value init)
        throws Fail.Exception {
        final int size = len.asLong();
        if (size == 0) {
            return ctxt.getContext().getAtom(0);
        } else if (init.isBlock()
                   && (init.asBlock().getTag() == Block.DOUBLE_TAG)) {
            final double d = init.asBlock().asDouble();
            if (size * Block.DOUBLE_SIZE > Block.MAX_WOSIZE) {
                Fail.invalidArgument("Array.make");
            } // end if
            final Block bl = Block.createDoubleArray(size);
            for (int i = 0; i < size; i++) {
                bl.setDouble(i, d);
            } // end for
            return Value.createFromBlock(bl);
        } else {
            if (size > Block.MAX_WOSIZE) {
                Fail.invalidArgument("Array.make");
            } // end if
            final Block bl = Block.createBlock(size, 0);
            for (int i = 0; i < size; i++) {
                bl.set(i, init);
            } // end for
            return Value.createFromBlock(bl);
        } // end if/elsif/else
    } // end method 'caml_make_vect(CodeRunner, Value, Value)'

    /**
     * Creates a block/array.
     * @param ctxt context
     * @param init value to copy
     * @return if <tt>init</tt> has a size of 0 or if first element of init is
     *         either a long or not a double then <tt>init</tt> is returned
     *         <br/>
     *         otherwise, a copy of <tt>init</tt> as a double array is returned
     */
    @Primitive
    public static Value caml_make_array(final CodeRunner ctxt,
                                        final Value init) {
        final Block block = init.asBlock();
        final int size = block.getWoSize();
        if (size == 0) {
            return init;
        } else {
            final Value v = block.get(0);
            if (v.isLong() || (v.asBlock().getTag() != Block.DOUBLE_TAG)) {
                return init;
            } else {
                final Block res = Block.createDoubleArray(size);
                for (int i = 0; i < size; i++) {
                    res.setDouble(i, block.get(i).asBlock().asDouble());
                } // end for
                return Value.createFromBlock(res);
            } // end if/else
        } // end if/else
    } // end method 'caml_make_array(CodeRunner, Value)'

} // end class 'Array'
